/**
 * 
 */
package neptune.manager;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.InputStreamReader;
import java.util.Map;
import neptune.manager.allocator.AllocationException;
import neptune.manager.allocator.Allocator;
import neptune.manager.allocator.DeallocationException;
import neptune.manager.allocator.LKAllocationMap;
import neptune.manager.allocator.LKAllocationMapEnhanced;
import neptune.manager.allocator.TerminationException;
import neptune.manager.builder.BuilderException;
import neptune.manager.builder.ControlIPAddressBuilder;
import neptune.manager.builder.ControlNetBuilder;
import neptune.manager.builder.ExperimentSubnetBuilder;
import neptune.manager.builder.IPAddressRange;
import neptune.manager.completer.CompleterException;
import neptune.manager.completer.LinkCompleter;
import neptune.manager.completer.NetworkInterfaceCompleter;
import neptune.manager.completer.NodeCompleter;
import neptune.manager.completer.OLPILinkCompleter;
import neptune.manager.completer.SimpleNodeCompleter;
import neptune.manager.completer.TopologyCompleter;
import neptune.manager.configurator.Configurator;
import neptune.manager.configurator.ConfiguratorException;
import neptune.manager.connector.Connector;
import neptune.manager.connector.ConnectorException;
import neptune.manager.connector.LibvirtXenConnector;
import neptune.manager.connector.XenStatistic;
import neptune.manager.model.ApplicationProperties;
import neptune.manager.model.cluster.Cluster;
import neptune.manager.model.cluster.PhysicalNode;
import neptune.manager.model.experiment.Experiment;
import neptune.manager.model.experiment.ExperimentState;
import neptune.manager.model.experiment.TopologyException;
import neptune.manager.model.topology.IPAddress;
import neptune.manager.model.topology.MACAddress;
import neptune.manager.model.topology.NetworkInterface;
import neptune.manager.model.topology.Topology;
import neptune.manager.model.topology.UndefinedControlInterfaceException;
import neptune.manager.storage.ApplicationPropertiesStorageFactory;
import neptune.manager.storage.ClusterStorageFactory;
import neptune.manager.storage.ExperimentRepositoryException;
import neptune.manager.storage.ExperimentsRepository;
import neptune.manager.storage.StorageReadException;
import neptune.manager.storage.StorageSaveException;
import neptune.manager.storage.TemplateStorageFactory;
import neptune.manager.storage.TemplateStorage;
import neptune.manager.storage.TopologyDefinitionException;
import neptune.manager.storage.TopologyStorage;
import neptune.manager.storage.TopologyStorageFactory;
import neptune.manager.storage.VmTemplateRepository;
import neptune.manager.storage.XMLTopologyStorage;
import neptune.manager.utils.DebugLog;
import neptune.manager.utils.Logger;
import neptune.manager.utils.SSHConnectionHandler;
import neptune.manager.utils.Logger.LogContext;
import neptune.manager.utils.Logger.MessageType;
import neptune.manager.validator.ValidatorResult;
import neptune.manager.validator.Validator.Status;
import neptune.manager.validator.allocability.AllocabilityTopologyValidator;
import neptune.manager.validator.core.CoreTopologyValidator;

/**
 * This class is a facade.
 * In questa classe sono realizzati tutti i meccanismi di gestione degli
 * esperimenti e del cluster. 
 * 
 * @author Roberto Bifulco [info@robertobifulco.it, robertobifulco.it]
 * @author Arpino Luigi Lorenzo [luigiarpino@yahoo.it]
 * 
 */
public class NeptuneManager {

	public static final String EXPID_NODEID_SEPARATOR = ":";
	private static NeptuneManager instance = null;

	/**
	 * Restituisce il nome dell'esperimento cui appartiene il nodo identificato
	 * dal nome completo passato come argomento. Oppure, restituisce null se il
	 * formato del nome completo non e' corretto.
	 * 
	 * @param nodeFullId
	 * @return
	 */
	public static String getExpNameFromNodeFullID(String nodeFullId) {
		if (nodeFullId.split(NeptuneManager.EXPID_NODEID_SEPARATOR).length != 2)
			return null;

		return nodeFullId.split(NeptuneManager.EXPID_NODEID_SEPARATOR)[1];
	}

	/**
	 * Restituisce il nome del nodo dal nome completo di nodo passato come
	 * argomento. Oppure, restituisce null se il formato del nome completo non
	 * e' corretto.
	 * 
	 * @param nodeFullId
	 * @return
	 */
	public static String getNodeNameFromNodeFullID(String nodeFullId) {
		if (nodeFullId.split(NeptuneManager.EXPID_NODEID_SEPARATOR).length != 2)
			return null;

		return nodeFullId.split(NeptuneManager.EXPID_NODEID_SEPARATOR)[0];
	}

	public static NeptuneManager instance() {
		return instance;
	}

	/**
	 * Il repository di tutti gli esperimenti gestiti dall'applicazione
	 */
	private ExperimentsRepository experimentsRepository;
	/**
	 * La factory per ottenere il cluster di server su cui opera l'applicazione
	 */
	private final ClusterStorageFactory clusterStorageFactory;
	/**
	 * La factory per ottenere la configurazione dell'applicazione
	 */
	private final ApplicationPropertiesStorageFactory applicationPropStorageFactory;
	/**
	 * La factory che fornisce i TopologyStorage che servono a recuperare la
	 * topologia degli esperimenti
	 */
	private TopologyStorageFactory topoplogyStorageFactory;
	/**
	 * La factory che fornisce i
	 * 
	 */
	private final TemplateStorageFactory templateStorageFactory;

	private Allocator allocator;
	private Configurator configurator;
	private ControlNetBuilder controlNetBuilder;

	private IPAddressRange controlIPRange;
	private String linkSubnet;

	private ExperimentSubnetBuilder expSubnetBuilder;

	private final VmTemplateRepository vmTemplateRepository;

	/**
	 * L'inizializzazione della classe avviene definendo il repository in cui si
	 * conservano tutte le informazioni riguardanti gli esperimenti, ed il
	 * cluster su cui gli esperimenti sono eseguiti. <br>
	 * <br>
	 * Viene inoltre fornita una Factory per i TopologyStorage, il range di
	 * indirizzi IP in cui possono essere definiti indirizzi della control net,
	 * l'indirizzo di subnet base da cui verranno generati tutti gli indirizzi
	 * di subnet assegnati agli esperimenti, l'indirizzo di subnet generale cui
	 * appartengono tutte le subnet degli esperimenti.
	 * 
	 * 
	 * @param experimentsRepository
	 * @param topStorageFactory
	 * @param cluster
	 * @param expsAllocator
	 * @param expsConfigurator
	 * @param controlIPRange
	 */
	public NeptuneManager(ExperimentsRepository experimentsRepository,
			TopologyStorageFactory topStorageFactory,
			ClusterStorageFactory clusterFactory,
			TemplateStorageFactory templateFactory,
			ApplicationPropertiesStorageFactory appPropStorageFactory,
			Allocator expsAllocator, Configurator expsConfigurator,
			IPAddressRange controlIPRange, IPAddress baseExperimentSubnet,
			IPAddress clusterSubnet, String linkSubnet,
			VmTemplateRepository vmTemplateRepository) {
		this.experimentsRepository = experimentsRepository;
		this.topoplogyStorageFactory = topStorageFactory;
		this.clusterStorageFactory = clusterFactory;
		this.templateStorageFactory = templateFactory;
		this.applicationPropStorageFactory = appPropStorageFactory;
		this.allocator = expsAllocator;
		this.configurator = expsConfigurator;
		this.controlIPRange = controlIPRange;
		this.linkSubnet = linkSubnet;
		this.vmTemplateRepository = vmTemplateRepository;
		ControlIPAddressBuilder ipBuilder = new ControlIPAddressBuilder(
				experimentsRepository, topStorageFactory, controlIPRange);
		// MACAddressBuilder macBuilder = new MACAddressBuilder(
		// experimentsRepository, topStorageFactory);
		this.controlNetBuilder = new ControlNetBuilder(ipBuilder);
		expSubnetBuilder = new ExperimentSubnetBuilder(baseExperimentSubnet,
				clusterSubnet, this);

		instance = this;
	}

	/**
	 * Crea una richiesta di allocazione per il dato esperimento
	 * 
	 * @param experiment
	 */
	public void addExperimentToAllocationQueue(Experiment experiment)
			throws AllocationRequestException {
		// TODO verificare la necessita' di eseguire altre operazioni/controlli

		// Imposta lo stato dell'esperimento a WAITING
		if (experiment.getState().getValue().equals(
				ExperimentState.StatesID.DEFINED_LABEL))
			experiment.getState().setValue(
					ExperimentState.StatesID.WAITING_LABEL);
		else
			throw new AllocationRequestException(
					"Unable to put experiment in 'WAITING' state. "
							+ "Experiment state is not 'DEFINED'");
		try {
			experimentsRepository.updateExperiment(experiment);
		} catch (ExperimentRepositoryException e) {
			throw new RuntimeException(e);
		}
	}

	/**
	 * Crea una richiesta di allocazione per il dato esperimento
	 * 
	 * @param experiment
	 */
	public void addExperimentToAllocationQueue(String expName)
			throws AllocationRequestException {

		// Imposta lo stato dell'esperimento a WAITING
		try {
			String actualState = experimentsRepository
					.getExperimentState(expName);
			if (actualState.equals(ExperimentState.StatesID.DEFINED_LABEL))
				experimentsRepository.setExperimentState(expName,
						ExperimentState.StatesID.WAITING_LABEL);
			else
				throw new AllocationRequestException(
						"Unable to put experiment in 'WAITING' state. "
								+ "Experiment state is not 'DEFINED'");
		} catch (ExperimentRepositoryException e) {
			throw new RuntimeException(e);
		}
	}

	/**
	 * Aggiunge una macchina fisica al cluster
	 * 
	 * @param idPhysicalMachine
	 * @return
	 * @throws Exception
	 */
	public void addPhysicalMachine(PhysicalNode physicalNode) throws Exception {
		Cluster cluster = this.getCluster();
		String ipAddress = physicalNode.getControlAddress().getAddress();
		String passwdRoot = physicalNode.getOperativeSystem()
				.getAdminPassword();

		String utente = System.getProperty("user.name");
		String fileSeparator = System.getProperty("file.separator");
		String userHome = System.getProperty("user.home");
		String directorySSH = userHome + fileSeparator + ".ssh";

		SSHConnectionHandler handler = SSHConnectionHandler
				.createSSHConnectionHandler(ipAddress, 22, "root", passwdRoot);
		BufferedReader reader;
		boolean allRight = true;
		String msg = new String("Add new physical machine :");
		;
		int ram = 0;
		int numProcesori = 0;
		String cpuModel = "undefined";
		String cpuVendor = "undefined";
		int cpuClock = 0;
		String mac = "";
		String subnetMask = "255.255.255.0";
		Float hd = new Float(0);
		try {
			// Leggo la chiave pubblica
			String comando = "ls " + directorySSH;
			Process proc = Runtime.getRuntime().exec(comando);
			BufferedReader br = new BufferedReader(new InputStreamReader(proc
					.getInputStream()));
			// BufferedWriter bw =new BufferedWriter( new OutputStreamWriter(
			// proc.getOutputStream() ) );
			String chiavePubblica = "";
			String line;
			while ((line = br.readLine()) != null) {
				if (line.contains(".pub")) {
					chiavePubblica = line;
				}
			}
			br.close();
			if (chiavePubblica.equalsIgnoreCase(""))
				throw new java.lang.Exception(
						"RSA public key not present for user " + utente);
			// Leggo il contenuto della chiave pubblica
			BufferedReader fIn = new BufferedReader(new FileReader(directorySSH
					+ fileSeparator + chiavePubblica));
			String contenutoChiave = "";
			String linea;
			while ((linea = fIn.readLine()) != null) {
				contenutoChiave += linea;
			}
			fIn.close();
			// Verifico se la chiave è già presente sul server
			handler.sendCommand("cat /root/.ssh/authorized_keys | grep \""
					+ contenutoChiave + "\" | wc -l");
			reader = handler.getStdOut();
			reader = handler.getStdOut();
			int numChiave = Integer.valueOf(reader.readLine().trim());
			if (numChiave == 0) {
				// La chiave non è presente sul server
				// scrivo la chiave pubblica sul server
				handler.sendCommand("mkdir /root/.ssh");
				handler.sendCommand("echo \"" + contenutoChiave
						+ "\" >> /root/.ssh/authorized_keys ");
				// verifico la presenza della chiave pubblica sul server
				handler.sendCommand("cat /root/.ssh/authorized_keys | grep \""
						+ contenutoChiave + "\" | wc -l");
				reader = handler.getStdOut();
				numChiave = Integer.valueOf(reader.readLine().trim());
				if (numChiave < 1)
					throw new Exception(
							"Unable to copy RSA public key on server "
									+ ipAddress);
			}
		} catch (java.lang.Exception e) {
			e.printStackTrace();
			throw new Exception("\n Unable to copy RSA public key on server "
					+ ipAddress);
		}
		try {
			// Leggo la memoria RAM
			handler
					.sendCommand("cat /proc/meminfo |grep MemTotal |cut -f 1 -dk |cut -f 1 -d: --complement");
			reader = handler.getStdOut();
			ram = Integer.valueOf(reader.readLine().trim());
			ram = ram / 1024;
		} catch (java.lang.Exception e) {
			allRight = false;
			msg += " unable to read amount of RAM";
		}
		try {
			// Leggo il numero di CPU
			handler.sendCommand("cat /proc/cpuinfo |grep processor|wc --lines");
			reader = handler.getStdOut();
			numProcesori = Integer.valueOf(reader.readLine().trim());
		} catch (java.lang.Exception e) {
			allRight = false;
			msg += " unable to read amount of CPU";
		}

		try {
			// Leggo il modello di cpu
			handler
					.sendCommand("cat /proc/cpuinfo |grep \"model name\"|cut -f 2 -d:");
			reader = handler.getStdOut();
			cpuModel = reader.readLine().trim();
		} catch (java.lang.Exception e) {
			allRight = false;
			msg += " unable to read model of CPU";
		}
		try {
			// Leggo il produttore di cpu
			handler
					.sendCommand("cat /proc/cpuinfo |grep \"vendor_id\"|cut -f 2 -d:");
			reader = handler.getStdOut();
			cpuVendor = reader.readLine().trim();
		} catch (java.lang.Exception e) {
			allRight = false;
			msg += " unable to read CPU vendor";
		}
		try {
			// Leggo la frequenza del processore
			handler
					.sendCommand("cat /proc/cpuinfo |grep \"cpu MHz\"|cut -f 2 -d:|cut -f 1 -d.");
			reader = handler.getStdOut();
			cpuClock = Integer.parseInt(reader.readLine().trim());
		} catch (java.lang.Exception e) {
			allRight = false;
			msg += " Unable to read cpu MHz!";
		}
		try {
			// Leggo il MAC Address
			handler
					.sendCommand(" ifconfig|grep -B1 \""
							+ ipAddress
							+ "\"|grep HWaddr| cut -f 1 -d: --complement| cut -f 4 \"-d \"");
			reader = handler.getStdOut();
			mac = reader.readLine().trim();
		} catch (java.lang.Exception e) {
			allRight = false;
			msg += " Unable to read MAC address!";
		}
		try {
			// Leggo la netmask
			handler.sendCommand("ifconfig | grep \"inet addr:" + ipAddress
					+ "\"|cut -f 4 -d:");
			reader = handler.getStdOut();
			subnetMask = reader.readLine().trim();
		} catch (java.lang.Exception e) {
			allRight = false;
			msg += " Unable to read subnet mask!";
		}
		try {
			// Leggo HD
			handler
					.sendCommand(" df  --block-size=1073741824 |grep -v \"Filesystem\"| awk '{print $2}'");
			reader = handler.getStdOut();
			String line;
			while ((line = reader.readLine()) != null) {
				if (!line.equalsIgnoreCase(""))
					hd = hd + new Float(line);
			}
		} catch (java.lang.Exception e) {
			allRight = false;
			msg += " Unable to read HD space!";
		}
		handler.dispose();

		// Setto i dati
		physicalNode.setId(String.valueOf(System.currentTimeMillis()));
		physicalNode.getCpu().setAmount(numProcesori);
		physicalNode.getCpu().setClock(cpuClock);
		physicalNode.getCpu().setName(cpuVendor);
		physicalNode.getCpu().setType(cpuModel);

		NetworkInterface[] netInt = physicalNode.getInterfaces();
		MACAddress macAddress = new MACAddress(mac);
		netInt[0].setMacAddress(macAddress);
		IPAddress ip = physicalNode.getControlAddress();
		ip.setSubnetMask(subnetMask);
		physicalNode.setHd(Float.valueOf(hd));
		physicalNode.setRam(ram);
		physicalNode.getCpu().setAmount(numProcesori);
		cluster.addPhysicalNode(physicalNode);
		this.saveCluster(cluster);
		// LOG
		if (!allRight)
			DebugLog
					.printDetailedLog(msg, NeptuneManager.class.getSimpleName());
	}

	/**
	 * Alloca l'esperimento, il cui id e' passato come argomento, sul cluster.
	 * L'allocazione avviene per mezzo dell'allocator definito nel costruttore
	 * dell'ExperimentManager.
	 * 
	 * @param expId
	 * @throws ExperimentRepositoryException
	 * @throws AllocationException
	 */
	public LogContext allocateExperiment(String expId)
			throws ExperimentRepositoryException, AllocationException {
		return allocateExperiment(expId, allocator);
	}

	/**
	 * Effettua l'allocazione dell'esperimento utilizzando l'algoritmo di
	 * Lin-Kernighan
	 * 
	 * @param expId
	 *            id dell'esperimento
	 * @param maxVMsPerNode
	 *            massimo numero di macchine virtuali per macchiana fisica
	 * @param maxBandwidth
	 *            massima banda di un link fi connessione tra due macchine
	 *            fisiche
	 * 
	 * @return
	 * @throws ExperimentRepositoryException
	 * @throws AllocationException
	 * @throws StorageReadException
	 */
	public String allocateExperimentWithLK(String expId, int maxVMsPerNode,
			int maxBandwidth) throws ExperimentRepositoryException,
			AllocationException, StorageReadException {
		Experiment experiment = getExperiment(expId);
		Cluster cluster = clusterStorageFactory.getClusterStorage()
				.getCluster();
		LKAllocationMap lkMap = new LKAllocationMap(experiment, cluster,
				maxVMsPerNode, maxBandwidth);
		Map<String, String[]> allocationMap = lkMap.getMap();
		// Alloco l'esperimento
		allocateExperiment(expId, allocator, allocationMap);
		String message = lkMap.getMessaggio();
		// logContext.addMessage(message, MessageType.NORMAL);
		return message;
	}

	public String allocateExperimentWithLKShared(String expId,
			int maxVMsPerNode, int maxBandwidth)
			throws ExperimentRepositoryException, AllocationException,
			StorageReadException {
		Experiment experiment = getExperiment(expId);
		Cluster cluster = clusterStorageFactory.getClusterStorage()
				.getCluster();
		LKAllocationMapEnhanced lkMapEnhanced = new LKAllocationMapEnhanced(
				experiment, cluster, maxVMsPerNode, maxBandwidth);
		Map<String, String[]> allocationMap = lkMapEnhanced.getMap();
		// Alloco l'esperimento
		allocateExperiment(expId, allocator, allocationMap);
		String message = lkMapEnhanced.getMessaggio();
		// logContext.addMessage(message, MessageType.NORMAL);
		return message;
	}

	/**
	 * Alloca l'esperimento, il cui id e' passato come argomento, sul cluster.
	 * L'allocazione avviene per mezzo dell'allocator passato come argomento.
	 * Prima di effettuare l'allocazione viene valutata la control network
	 * associata all'esperimento e, se necessario, vengono modificati i nodi per
	 * supportare la control network. <br>
	 * <br>
	 * Poiche' il procedimento di allocazione dell'esperimento prevede che la
	 * topologia associata sia controllata ed eventualmente completata, in caso
	 * di errore il testo dell'eccezione sollevata contiene un LOG delle
	 * attivita' compiute dal sistema.
	 * 
	 * @param expId
	 * @param allocator
	 * @throws ExperimentRepositoryException
	 * @throws AllocationException
	 */
	public LogContext allocateExperiment(String expId, Allocator allocator)
			throws ExperimentRepositoryException, AllocationException {
		return allocateExperiment(expId, allocator, null);
	}

	/**
	 * Alloca l'esperimento, il cui id e' passato come argomento, sul cluster.
	 * L'allocazione avviene per mezzo dell'allocator passato come argomento.
	 * Prima di effettuare l'allocazione viene valutata la control network
	 * associata all'esperimento e, se necessario, vengono modificati i nodi per
	 * supportare la control network. <br>
	 * <br>
	 * Poiche' il procedimento di allocazione dell'esperimento prevede che la
	 * topologia associata sia controllata ed eventualmente completata, in caso
	 * di errore il testo dell'eccezione sollevata contiene un LOG delle
	 * attivita' compiute dal sistema.
	 * 
	 * @param expId
	 * @param allocator
	 * @throws ExperimentRepositoryException
	 * @throws AllocationException
	 */
	public LogContext allocateExperiment(String expId, Allocator allocator,
			Map<String, String[]> allocationMap)
			throws ExperimentRepositoryException, AllocationException {
		Experiment exp = getExperiment(expId);

		if (exp.getState().getValue().equals(
				ExperimentState.StatesID.UNDEFINED_LABEL))
			throw new AllocationException(
					"Experiments in UNDEFINED state are not allocable.");
		if (exp.getState().getValue().equals(
				ExperimentState.StatesID.ALLOCATED_LABEL)
				|| exp.getState().getValue().equals(
						ExperimentState.StatesID.RUNNING_LABEL))
			throw new AllocationException(
					"The experiment is already allocated, "
							+ "to make a new allocation, you need to swap it out first.");

		LogContext logContext = Logger.createLogContext("ExperimentAllocation");
		logContext.addMessage("Starting experiment '" + expId
				+ "' allocability validation", MessageType.NORMAL);

		try {
			logContext.addMessage("Starting allocability validation",
					MessageType.NORMAL);
			ValidatorResult validation = exp
					.validate(new AllocabilityTopologyValidator(this, exp
							.getAssignedSubnet()));

			logContext.addContextMessage(validation.getLog());

			if (validation.getStatus() == Status.ERROR) {

				// TODO inserire supporto alla completion
				// verifica e/o prepara la control net
				controlNetBuilder.prepareControlNetwork(exp.getTopology(),
						logContext.createChild("controlNetBuilder"));

				NodeCompleter nodeComp = new SimpleNodeCompleter(
						new NetworkInterfaceCompleter());
				LinkCompleter linkComp = new OLPILinkCompleter();
				TopologyCompleter completer = new TopologyCompleter(nodeComp,
						linkComp);

				try {
					completer.completeTopology(exp.getTopology());
				} catch (CompleterException e) {
					throw new AllocationException(e);
				}

				logContext.addMessage("Topology completion done. Saving...",
						MessageType.NORMAL);
				// Salva la topologia modificata per supportare la control net
				topoplogyStorageFactory.getTopologyStorage(expId).saveTopology(
						exp.getTopology());
				logContext.addMessage("Topology saved", MessageType.NORMAL);

				// Convalida nuovamente la topologia per verificare se il
				// processo di completion ha avuto successo.
				logContext.addMessage("Starting allocability validation",
						MessageType.NORMAL);
				validation = exp.validate(new AllocabilityTopologyValidator(
						this, exp.getAssignedSubnet()));
				logContext.addContextMessage(validation.getLog());

				// TODO da modificare, la non allocabilita' della topologia non
				// e' un'eccezione del sistema, ma un normale comportamente che
				// deve essere gestito in modo alternativo.
				if (validation.getStatus() == Status.ERROR)
					throw new AllocationException(
							"Topology is not allocable.\nLog:\n"
									+ logContext.prettyPrint(0));
			}
		} catch (TunerException e) {
			throw new AllocationException(e + "\nLog:\n"
					+ logContext.prettyPrint(0));
		} catch (StorageSaveException e) {
			throw new AllocationException(
					"Unable to save topology after control net settings. Cause: "
							+ e.getMessage() + "\nLog:\n"
							+ logContext.prettyPrint(0));
		} finally {
			DebugLog.printDetailedLog("Allocation log: "
					+ logContext.prettyPrint(0), NeptuneManager.class
					.getSimpleName());
		}

		try {

			logContext.addMessage("Starting experiment '" + expId
					+ "' allocation.", MessageType.NORMAL);

			Cluster cluster = clusterStorageFactory.getClusterStorage()
					.getCluster();

			logContext.addMessage("Swapping in", MessageType.NORMAL);
			if (allocationMap != null)
				exp.swapIn(allocator, cluster, allocationMap);
			else
				exp.swapIn(allocator, cluster);

			logContext.addMessage("Updating experiment data",
					MessageType.NORMAL);
			experimentsRepository.updateExperiment(exp);

			logContext.addMessage("Updating cluster state", MessageType.NORMAL);
			clusterStorageFactory.getClusterStorage().saveCluster(cluster);

		} catch (StorageReadException e) {
			logContext.addMessage("Cluster read problem", MessageType.ERROR);
			throw new AllocationException(e + "\nLog:\n"
					+ logContext.prettyPrint(0));
		} catch (StorageSaveException e) {
			// TODO Auto-generated catch block
			logContext.addMessage("Cluster state saving problem",
					MessageType.ERROR);
			e.printStackTrace();
			throw new AllocationException(e + "\nLog:\n"
					+ logContext.prettyPrint(0));
		}

		return logContext;
	}

	/**
	 * Alloca l'esperimento, il cui id e' passato come argomento, sul cluster.
	 * L'allocazione avviene per mezzo dell'allocator definito nel costruttore
	 * dell'ExperimentManager.
	 * 
	 * @param expId
	 * @throws ExperimentRepositoryException
	 * @throws AllocationException
	 */
	public LogContext allocateExperiment(String expId,
			Map<String, String[]> allocationMap)
			throws ExperimentRepositoryException, AllocationException {
		return allocateExperiment(expId, allocator, allocationMap);
	}

	/**
	 * Configura l'esperimento, il cui id e' passato come argomento, sul
	 * cluster. La configurazione avviene per mezzo del configurator definito al
	 * momento della costruzione dell'ExperimentManager.
	 * 
	 * @param expId
	 * @throws ExperimentRepositoryException
	 * @throws ConfiguratorException
	 */
	public LogContext configureExperiment(String expId)
			throws ExperimentRepositoryException, ConfiguratorException {
		return configureExperiment(expId, configurator);
	}

	/**
	 * Configura l'esperimento, il cui id e' passato come argomento, sul
	 * cluster. La configurazione avviene per mezzo del configurator passato
	 * come argomento.
	 * 
	 * @param expId
	 * @param configurator
	 * @throws ExperimentRepositoryException
	 * @throws ConfiguratorException
	 */
	public LogContext configureExperiment(String expId,
			Configurator configurator) throws ExperimentRepositoryException,
			ConfiguratorException {

		LogContext log = new LogContext("ExperimentConfiguration");

		log.addMessage("Starting experiment configuration", MessageType.NORMAL);
		Experiment exp = getExperiment(expId);
		exp.setUp(configurator);

		log.addMessage("Saving experiment state", MessageType.NORMAL);
		experimentsRepository.updateExperiment(exp);

		return log;
	}

	/**
	 * Crea un nuovo esperimento, specificato tramite un oggetto Experiment, ne
	 * definisce la subnet assegnata e lo aggiunge al repository degli
	 * esperimenti.
	 * 
	 * @param experiment
	 * @throws ExperimentException
	 */
	public void createExperiment(Experiment experiment)
			throws ExperimentException, ExperimentRepositoryException {
		try {
			experimentsRepository.addExperimentToRepository(experiment);
			// Impostazione del timestamp di creazione
			experiment.getState().setCreationTimestamp(
					System.currentTimeMillis());
			experiment.setAssignedSubnet(expSubnetBuilder
					.generateExperimentSubnet());
			experimentsRepository.updateExperiment(experiment);
		} catch (BuilderException e) {
			throw new ExperimentException(e);
		}
	}

	/**
	 * Crea un nuovo esperimento, specificato tramite Identificativo ed
	 * amministratore, ne definisce la subnet assegnata e lo aggiunge al
	 * repository degli esperimenti.
	 * 
	 * @param experiment
	 * @throws ExperimentException
	 */
	public void createExperiment(String expName, String expAdmin)
			throws ExperimentException, ExperimentRepositoryException {
		try {
			experimentsRepository.createExperiment(expName, expAdmin, System
					.currentTimeMillis());
			Experiment exp = getExperiment(expName);
			exp.setAssignedSubnet(expSubnetBuilder.generateExperimentSubnet());
			experimentsRepository.updateExperiment(exp);
		} catch (BuilderException e) {
			throw new ExperimentException(e);
		}

	}

	/**
	 * Dealloca un esperimento. Dopo questa operazione e' sempre possibile
	 * riallocare nuovamente un esperimento, a differenza di quanto avviene con
	 * una operazione Terminate, che rende la deallocazione definitiva.
	 * 
	 * @param expId
	 * @throws ExperimentRepositoryException
	 * @throws DeallocationException
	 */
	public void deallocateExperiment(String expId)
			throws ExperimentRepositoryException, DeallocationException {
		deallocateExperiment(expId, allocator);
	}

	/**
	 * Dealloca un esperimento. Dopo questa operazione e' sempre possibile
	 * riallocare nuovamente un esperimento, a differenza di quanto avviene con
	 * una operazione Terminate, che rende la deallocazione definitiva. Per
	 * eseguire la deallocazione e' adoperato l'Allocator passato come
	 * argomento.
	 * 
	 * @param expId
	 * @param allocator
	 * @throws ExperimentRepositoryException
	 * @throws DeallocationException
	 */
	public void deallocateExperiment(String expId, Allocator allocator)
			throws ExperimentRepositoryException, DeallocationException {
		Experiment exp = getExperiment(expId);
		try {
			Cluster cluster = clusterStorageFactory.getClusterStorage()
					.getCluster();
			exp.swapOut(allocator, cluster);
			experimentsRepository.updateExperiment(exp);
			clusterStorageFactory.getClusterStorage().saveCluster(cluster);
		} catch (StorageReadException e) {
			throw new DeallocationException(e);
		} catch (StorageSaveException e) {
			// TODO
			e.printStackTrace();
		}
	}

	/**
	 * Rimuove un esperimento, eseguendone lo swap-out, e terminando ogni
	 * attivita' di monitoraggio dello stesso
	 * 
	 * @param experimentID
	 * @throws ExperimentRepositoryException
	 * @throws DeallocationException
	 */
	public boolean deleteExperiment(String experimentID)
			throws ExperimentRepositoryException, DeallocationException {
		if (existExperiment(experimentID)) {
			Experiment exp = getExperiment(experimentID);

			exp.swapOut(allocator, getCluster());

			boolean isExpRemoved = experimentsRepository
					.deleteExperiment(experimentID);
			// boolean isTopologyRemoved =
			// topoplogyStorageFactory.getTopologyStorage(experimentID).deleteTopology();
			topoplogyStorageFactory.getTopologyStorage(experimentID)
					.deleteTopology();
			return isExpRemoved;
		} else {
			return false;
		}
	}

	public void editPhysicalMachine(PhysicalNode physicalNode) {
		Cluster cluster = this.getCluster();
		cluster.editPhysicalNode(physicalNode);
		this.saveCluster(cluster);
	}

	public String[][] getAllTemplate() {
		try {
			return this.templateStorageFactory.getTemplateStorage()
					.getAllTemplate();
		} catch (StorageReadException e) {
			throw new RuntimeException(e);
		}
	}

	public void deleteTemplate(String nomeFile) {
		try {
			this.templateStorageFactory.getTemplateStorage().deleteTemplate(
					nomeFile);
		} catch (StorageSaveException e) {
			throw new RuntimeException(e);
		}
	}

	public ApplicationProperties getApplicationProperties() {
		try {
			return this.applicationPropStorageFactory
					.getApplicationPropertiesStorage()
					.getApplicationProperties();
		} catch (StorageReadException e) {
			throw new RuntimeException(e);
		}
	}

	/**
	 * @return the cluster
	 */
	public Cluster getCluster() {
		try {
			return clusterStorageFactory.getClusterStorage().getCluster();
		} catch (StorageReadException e) {
			// Se non e' possibile accedere al cluster, l'applicazione non puo'
			// funzionare, quindi viene lanciata una eccezione che risale al
			// livello utente.
			throw new RuntimeException(e);
		}
	}

	/**
	 * 
	 * @param physicalMachineId
	 * @return
	 * @throws ConnectorException
	 */
	public Connector getConnector(String physicalMachineId)
			throws ConnectorException {
		try {
			// TODO LUIGI
			System.out.println("physicalMachineId " + physicalMachineId);
			PhysicalNode node = clusterStorageFactory.getClusterStorage()
					.getCluster().getNodeById(physicalMachineId);
			if (node == null)
				; // TODO
			String ipAddress = node.getControlAddress().getAddress();
			if (ipAddress == null)
				; // TODO
			// int controlPort = node.getControlPort();

			Connector connector = new LibvirtXenConnector(ipAddress, node
					.getControlPort(), node.getOperativeSystem()
					.getAdminPassword());
			return connector;
		} catch (StorageReadException e) {
			throw new ConnectorException(e);
		} catch (UndefinedControlInterfaceException e) {
			throw new ConnectorException(e);
		}
	}

	public IPAddressRange getControlIPAddresRange() {
		return controlIPRange;
	}

	/**
	 * Verifica l'esistenza di un esperimento
	 * 
	 * @param expId
	 * @return
	 * @throws ExperimentRepositoryException
	 */
	public boolean existExperiment(String expId)
			throws ExperimentRepositoryException {
		Experiment exp = experimentsRepository.getExperimentByName(expId);
		if (exp != null) {
			return true;
		} else {
			return false;
		}
	}

	/**
	 * Carica l'esperimento dall'ExperimentsRepository, ottenendo tutti i dati
	 * che lo caratterizzano, compresa la topologia (Adoperando un
	 * TopologyStorage).
	 * 
	 * @param expId
	 * @return
	 * @throws ExperimentRepositoryException
	 */
	public Experiment getExperiment(String expId)
			throws ExperimentRepositoryException {
		Experiment exp = experimentsRepository.getExperimentByName(expId);

		TopologyStorage topologyStorage = topoplogyStorageFactory
				.getTopologyStorage(expId);
		try {
			exp.setTopologyWithoutControls(topologyStorage.getTopology());
		} catch (StorageReadException e) {
			// Se la topologia non e' definita, aggiunge comunque una topologia
			// vuota.
			Topology topology = new Topology();
			topology.setName(expId);
			exp.setTopologyWithoutControls(topology);
			return exp;
		} catch (TopologyDefinitionException e) {
			throw new ExperimentRepositoryException(e);
		}

		return exp;
	}

	/**
	 * Restituisce gli esperimenti definiti sul sistema
	 * 
	 * @return
	 * @throws ExperimentRepositoryException
	 */
	public Experiment[] getExperiments() throws ExperimentRepositoryException {
		String[] ids = this.experimentsRepository.getExperiments();
		Experiment[] exps = new Experiment[ids.length];

		for (int i = 0; i < exps.length; i++) {
			exps[i] = getExperiment(ids[i]);
		}

		return exps;
	}

	/**
	 * Restituisce gli id degli esperimenti definiti sul sistema
	 * 
	 * @return
	 * @throws ExperimentRepositoryException
	 */
	public String[] getExperimentsId() throws ExperimentRepositoryException {
		return this.experimentsRepository.getExperiments();
	}

	/**
	 * @return the experimentsRepository
	 */
	public ExperimentsRepository getExperimentsRepository() {
		return experimentsRepository;
	}

	public String getLinksSubnetMask() {
		return linkSubnet;
	}

	/**
	 * Restituisce una macchina fisica dal cluster
	 * 
	 * @param id
	 * @return
	 */
	public PhysicalNode getPhysicalMachine(String id) {
		Cluster cluster = this.getCluster();
		PhysicalNode physicalNode = cluster.getNodeById(id);
		return physicalNode;
	}

	/**
	 * Restituisce il repository dei template software per una vm.
	 * 
	 * @return the vmTemplateRepository
	 */
	public VmTemplateRepository getVmTemplateRepository() {
		return vmTemplateRepository;
	}

	public String[][] getXenStatistic(String idPhysicalMachine) {
		try {
			String[] credential = this.clusterStorageFactory
					.getClusterStorage().getPhysicalMachineCredential(
							idPhysicalMachine);
			String password = credential[0];
			String sshPort = credential[1];
			String ipAddress = credential[2];
			return XenStatistic.getStatistic(ipAddress, password, sshPort);
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	public String getXMLtemplate(String fileTemplate) {
		try {
			return this.templateStorageFactory.getTemplateStorage()
					.getXMLtemplate(fileTemplate);
		} catch (StorageReadException e) {
			throw new RuntimeException(e);
		}
	}

	/**
	 * Ottiene la descrizione XML della topologia di un esperimento
	 * 
	 * @param idExperiment
	 * @return
	 * @throws Exception
	 */
	public String getXMLtopologyOfExperiment(String idExperiment)
			throws StorageReadException {
		TopologyStorage topStorage = this.topoplogyStorageFactory
				.getTopologyStorage(idExperiment);
		try {
			return XMLTopologyStorage.getXML(topStorage.getTopology());
		} catch (Exception e) {
			throw new StorageReadException(e.getMessage()
					+ " \n Unable to read xml topology of experiment "
					+ idExperiment);
		}
	}

	/**
	 * Rimuove un macchina fisica dal cluster
	 * 
	 * @param idPhysicalMachine
	 * @return
	 */
	public boolean removePhysicalMachine(String idPhysicalMachine) {
		Cluster cluster = this.getCluster();
		boolean isRemoved = cluster.removePhysicalNode(idPhysicalMachine);
		this.saveCluster(cluster);
		return isRemoved;
	}

	public void saveCluster(Cluster cluster) {
		try {
			this.clusterStorageFactory.getClusterStorage().saveCluster(cluster);
		} catch (StorageSaveException e) {
			// Se non e' possibile accedere al cluster, l'applicazione non puo'
			// funzionare, quindi viene lanciata una eccezione che risale al
			// livello utente.
			throw new RuntimeException(e);
		}
	}

	/**
	 * Salva l'esperimento con tutti i suoi dati, rendendolo persistente. Non
	 * effettua il salvataggio della topologia associata all'oggetto Experiment.
	 * 
	 * @param exp
	 * @throws ExperimentRepositoryException
	 */
	public void saveExperiment(Experiment exp)
			throws ExperimentRepositoryException {
		experimentsRepository.updateExperiment(exp);
	}

	/**
	 * Salva un template
	 * 
	 * @param topology
	 */
	public void saveTemplate(Topology topology) {
		try {
			TemplateStorage templateStorage = this.templateStorageFactory
					.getTemplateStorage();
			templateStorage.saveTemplate(topology);
		} catch (StorageSaveException e) {
			throw new RuntimeException(e);
		}
	}

	/**
	 * Associa la topologia all'esperimento il cui id e' passato come argomento
	 * e la rende persistente, se non ci sono errori.
	 * 
	 * @param expId
	 * @param topology
	 * @throws ExperimentRepositoryException
	 * @throws StorageSaveException
	 * @throws TopologyException
	 */
	public ValidatorResult saveExperimentTopology(String expId,
			Topology topology) throws ExperimentRepositoryException,
			StorageSaveException, TopologyException {
		Experiment exp = getExperiment(expId);

		ValidatorResult result = exp.setTopology(topology,
				new CoreTopologyValidator(this, exp.getAssignedSubnet()));

		saveExperiment(exp);
		topoplogyStorageFactory.getTopologyStorage(expId).saveTopology(
				exp.getTopology());

		return result;
	}

	/**
	 * Avvia l'esperimento, il cui id e' passato come argomento, sul cluster.
	 * L'allocazione la configurazione avviengono per mezzo degli oggetti
	 * allocator e configurator definiti nel costruttore dell'ExperimentManager.
	 * 
	 * @param expId
	 * @throws ExperimentRepositoryException
	 * @throws AllocationException
	 * @throws ConfiguratorException
	 */
	public LogContext startExperiment(String expId)
			throws ExperimentRepositoryException, AllocationException,
			ConfiguratorException {
		return startExperiment(expId, allocator, configurator);
	}

	/**
	 * Avvia l'esperimento, il cui id e' passato come argomento, sul cluster.
	 * L'allocazione la configurazione avviengono per mezzo degli oggetti
	 * allocator e configurator passati come argomento.
	 * 
	 * @param expId
	 * @param allocator
	 * @param configurator
	 * @throws ExperimentRepositoryException
	 * @throws AllocationException
	 * @throws ConfiguratorException
	 */
	public LogContext startExperiment(String expId, Allocator allocator,
			Configurator configurator) throws ExperimentRepositoryException,
			AllocationException, ConfiguratorException {
		return startExperiment(expId, allocator, configurator, null);
	}

	/**
	 * Avvia l'esperimento, il cui id e' passato come argomento, sul cluster.
	 * L'allocazione la configurazione avviengono per mezzo degli oggetti
	 * allocator e configurator passati come argomento. L'allocazione adopera
	 * l'allocation map passata come argomento.
	 * 
	 * @param expId
	 * @param allocator
	 * @param configurator
	 * @throws ExperimentRepositoryException
	 * @throws AllocationException
	 * @throws ConfiguratorException
	 */
	public LogContext startExperiment(String expId, Allocator allocator,
			Configurator configurator, Map<String, String[]> allocationMap)
			throws ExperimentRepositoryException, AllocationException,
			ConfiguratorException {

		LogContext logContext = new LogContext("Experiment-" + expId + "-Start");
		LogContext allocateContext = null;
		if (allocationMap == null)
			allocateContext = allocateExperiment(expId, allocator);
		else
			allocateContext = allocateExperiment(expId, allocator,
					allocationMap);

		LogContext configureContext = configureExperiment(expId, configurator);

		logContext.addContextMessage(allocateContext);
		logContext.addContextMessage(configureContext);

		return logContext;
	}

	/**
	 * Avvia l'esperimento, il cui id e' passato come argomento, sul cluster.
	 * L'allocazione la configurazione avviengono per mezzo degli oggetti
	 * allocator e configurator definiti nel costruttore dell'ExperimentManager.
	 * L'allocazione e' eseguita adoperando l'allocation map passata come
	 * argomento.
	 * 
	 * @param expId
	 * @param allocationMap
	 * @throws ExperimentRepositoryException
	 * @throws AllocationException
	 * @throws ConfiguratorException
	 */
	public LogContext startExperiment(String expId,
			Map<String, String[]> allocationMap)
			throws ExperimentRepositoryException, AllocationException,
			ConfiguratorException {
		return startExperiment(expId, allocator, configurator, allocationMap);
	}

	/**
	 * Termina in modo definitivo un esperimento. Non e' piu' possibile
	 * riallocare un esperimento terminato in questo modo.
	 * 
	 * @param expId
	 * @throws ExperimentRepositoryException
	 * @throws DeallocationException
	 */
	public void terminateExperiment(String expId)
			throws ExperimentRepositoryException, TerminationException {
		terminateExperiment(expId, allocator);
	}

	/**
	 * Termina in modo definitivo un esperimento. Non e' piu' possibile
	 * riallocare un esperimento terminato in questo modo. Per la terminazione
	 * e' adoperato l'Allocator passato come argomento.
	 * 
	 * @param expId
	 * @param allocator
	 * @throws ExperimentRepositoryException
	 * @throws DeallocationException
	 */
	public void terminateExperiment(String expId, Allocator allocator)
			throws ExperimentRepositoryException, TerminationException {
		Experiment exp = getExperiment(expId);
		try {
			Cluster cluster = getCluster();
			exp.terminate(allocator, cluster);
			experimentsRepository.updateExperiment(exp);
			clusterStorageFactory.getClusterStorage().saveCluster(cluster);
		} catch (StorageSaveException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	public void updateApplicationProperties(ApplicationProperties appProperties) {
		try {
			this.applicationPropStorageFactory
					.getApplicationPropertiesStorage()
					.setApplicationProperties(appProperties);
		} catch (StorageSaveException e) {
			throw new RuntimeException(e);
		}
	}

	/**
	 * 
	 * @param exp
	 * @return
	 * @throws ExperimentRepositoryException
	 */
	public ValidatorResult validateExperimentTopology(Experiment exp)
			throws ExperimentRepositoryException {
		ValidatorResult result = exp.validate(new CoreTopologyValidator(this,
				exp.getAssignedSubnet()));
		saveExperiment(exp);
		return result;
	}

}
